package com.openroom.editing;

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;

import org.apache.log4j.Logger;

import com.jhlabs.image.PixelUtils;

public class JPEGImageProcessor {

    private static final Logger LOGGER = Logger
            .getLogger(JPEGImageProcessor.class);
    int width = 0;
    int height = 0;
    private int bgColor = 0xffffffff; // white

    public int[] pixels = null;
    protected int[] snapshotPixels = null;

    protected int interpolationMethod = 0; // {NONE,BICUBIC,BILINEAR}
    private static double rWeight = 0.3333333333333333D;
    private static double gWeight = 0.3333333333333333D;
    private static double bWeight = 0.3333333333333333D;

    protected int roiX;
    protected int roiY;
    protected int roiWidth;
    protected int roiHeight;

    private int min = 0;
    private int max = 255;

    private int snapshotMin = 0;
    private int snapshotMax = 255;

    private int mode = -1;

    private int exposure = 0;
    private int shadows = 0;
    private int brightness = 0;
    private float contrast = 1;
    private float saturation = 1;

    public int getExposure() {
        return exposure;
    }

    public void setExposure(int exposure) {
        this.exposure = exposure;
    }

    public int getShadows() {
        return shadows;
    }

    public void setShadows(int shadows) {
        this.shadows = shadows;
    }

    public int getBrightness() {
        return brightness;
    }

    public void setBrightness(int brightness) {
        this.brightness = brightness;
    }

    public float getContrast() {
        return contrast;
    }

    public void setContrast(float contrast) {
        this.contrast = contrast;
    }

    public float getSaturation() {
        return saturation;
    }

    public void setSaturation(float saturation) {
        this.saturation = saturation;
    }

    private JPEGImageProcessor() {
        roiX = 0;
        roiY = 0;
        roiWidth = width;
        roiHeight = height;
    }

    public void resetRoi() {
        this.roiX = 0;
        this.roiY = 0;
        this.roiWidth = this.width;
        this.roiHeight = this.height;
    }

    public static JPEGImageProcessor createImageProcessor(int w, int h,
            int[] pixels) {
        JPEGImageProcessor cp = new JPEGImageProcessor();
        cp.width = w;
        cp.height = h;
        cp.pixels = new int[w * h];
        System.arraycopy(pixels, 0, cp.pixels, 0, w * h);
        return cp;
    }

    public void reset() {
        if (this.snapshotPixels == null)
            return;
        System.arraycopy(this.snapshotPixels, 0, this.pixels, 0, this.width
                * this.height);
    }

    public static JPEGImageProcessor createImageProcessor(BufferedImage image) {
        JPEGImageProcessor cp = new JPEGImageProcessor();
        int w = image.getWidth();
        int h = image.getHeight();
        cp.width = w;
        cp.height = h;
        cp.pixels = new int[w * h];
        for (int i = 0; i < w; i++) {
            for (int j = 0; j < h; j++) {
                int rgb = image.getRGB(i, j);
                cp.pixels[j * w + i] = rgb;
            }
        }
        return cp;
    }

    private JPEGImageProcessor cloneProcessor() {
        JPEGImageProcessor cp = new JPEGImageProcessor();
        cp.width = this.width;
        cp.height = this.height;
        cp.pixels = this.pixels;
        return cp;
    }

    private JPEGImageProcessor cloneEmptyProcessor(int width, int height) {
        JPEGImageProcessor cp = new JPEGImageProcessor();
        cp.width = width;
        cp.height = height;
        cp.pixels = new int[width * height];
        return cp;
    }

    public int getPixel(int x, int y) {
        if ((x >= 0) && (x < this.width) && (y >= 0) && (y < this.height)) {
            return this.pixels[(y * this.width + x)];
        }
        return 0;
    }

    public final void putPixel(int x, int y, int value) {
        if ((x >= 0) && (x < this.width) && (y >= 0) && (y < this.height))
            this.pixels[(y * this.width + x)] = value;
    }

    public void getRow(int x, int y, int[] data, int length) {
        for (int i = 0; i < length; i++)
            data[i] = getPixel(x++, y);
    }

    public void putColumn(int x, int y, int[] data, int length) {
        for (int i = 0; i < length; i++)
            putPixel(x, y++, data[i]);
    }

    public JPEGImageProcessor rotateRight() {
        int width2 = this.height;
        int height2 = this.width;
        JPEGImageProcessor clone = this.cloneEmptyProcessor(width2, height2);
        int[] arow = new int[this.width];
        for (int row = 0; row < this.height; row++) {
            getRow(0, row, arow, this.width);
            clone.putColumn(width2 - row - 1, 0, arow, height2);
        }
        return clone;
    }

    public void getColumn(int x, int y, int[] data, int length) {
        for (int i = 0; i < length; i++)
            data[i] = getPixel(x, y++);
    }

    public JPEGImageProcessor flipHorizontal() {
        JPEGImageProcessor clone = this.cloneProcessor();
        int[] col1 = new int[this.height];
        int[] col2 = new int[this.height];
        for (int x = 0; x < this.width / 2; x++) {
            clone.getColumn(0 + x, 0, col1, this.height);
            clone.getColumn(0 + this.width - x - 1, 0, col2, this.height);
            clone.putColumn(0 + x, 0, col2, this.height);
            clone.putColumn(0 + this.width - x - 1, 0, col1, this.height);
        }
        return clone;
    }

    public JPEGImageProcessor flipVertical() {
        JPEGImageProcessor clone = this.cloneProcessor();
        for (int y = 0; y < this.height / 2; y++) {
            int index1 = (0 + y) * this.width + 0;
            int index2 = (0 + this.height - 1 - y) * this.width + 0;
            for (int i = 0; i < this.width; i++) {
                int tmp = this.pixels[index1];
                clone.pixels[(index1++)] = this.pixels[index2];
                clone.pixels[(index2++)] = tmp;
            }
        }
        return clone;
    }

    public JPEGImageProcessor copyImage() {
        int width2 = this.width;
        int height2 = this.height;
        JPEGImageProcessor clone = this.cloneEmptyProcessor(width2, height2);
        int[] col1 = new int[this.height];
        for (int row = 0; row < this.width; row++) {
            getColumn(0 + row, 0, col1, this.height);
            clone.putColumn(0 + row, 0, col1, this.height);
        }
        return clone;
    }

    public JPEGImageProcessor rotateLeft() {
        int width2 = this.height;
        int height2 = this.width;
        JPEGImageProcessor clone = this.cloneEmptyProcessor(width2, height2);
        int[] arow = new int[this.width];
        int[] arow2 = new int[this.width];
        for (int row = 0; row < this.height; row++) {
            getRow(0, row, arow, this.width);
            for (int i = 0; i < this.width; i++) {
                arow2[i] = arow[(this.width - i - 1)];
            }
            clone.putColumn(row, 0, arow2, height2);
        }
        return clone;
    }

    public static void printColor(int px) {
        Color color = new Color(px);
        LOGGER.debug(String.format("%d-%d-%d\t", color.getRed(),
                color.getGreen(), color.getBlue()));
    }

    public BufferedImage createImage() {
        ColorModel cm = new DirectColorModel(24, 0xff0000, 0xff00, 0xff);
        WritableRaster raster = cm
                .createCompatibleWritableRaster(width, height);
        SampleModel sampleModel = raster.getSampleModel();
        DataBuffer dataBuffer = new DataBufferInt(pixels, pixels.length, 0);
        WritableRaster wr = Raster.createWritableRaster(sampleModel,
                dataBuffer, null);
        BufferedImage bufferedImage = new BufferedImage(cm,
                (WritableRaster) wr, false, null);
        return bufferedImage;
    }

    public BufferedImage createImage(int w, int h) {
        scale(w, h);
        ColorModel cm = new DirectColorModel(24, 0xff0000, 0xff00, 0xff);
        WritableRaster raster = cm
                .createCompatibleWritableRaster(width, height);
        SampleModel sampleModel = raster.getSampleModel();
        DataBuffer dataBuffer = new DataBufferInt(pixels, pixels.length, 0);
        WritableRaster wr = Raster.createWritableRaster(sampleModel,
                dataBuffer, null);
        BufferedImage bufferedImage = new BufferedImage(cm,
                (WritableRaster) wr, false, null);
        return bufferedImage;
    }

    public JPEGImageProcessor crop(int roiX, int roiY, int roiWidth,
            int roiHeight) {
        JPEGImageProcessor cloneProcessor = this;
        int[] pixels2 = new int[roiWidth * roiHeight];
        for (int ys = roiY; ys < roiY + roiHeight; ys++)
            System.arraycopy(pixels, roiX + ys * width, pixels2, (ys - roiY)
                    * roiWidth, roiWidth);
        cloneProcessor.pixels = pixels2;
        cloneProcessor.width = roiWidth;
        cloneProcessor.height = roiHeight;
        return cloneProcessor;
    }

    public Object getPixelsCopy() {
        int[] pixels2 = new int[width * height];
        System.arraycopy(pixels, 0, pixels2, 0, width * height);
        return pixels2;
    }

    public JPEGImageProcessor scale(int roiWidth, int roiHeight) {

        double xCenter = roiWidth / 2.0;
        double yCenter = roiHeight / 2.0;
        int xmin, xmax, ymin, ymax;

        xmin = 0;
        xmax = 0 + roiWidth;
        ymin = 0;
        ymax = 0 + roiHeight;
        double yScale = (double) roiHeight / height;
        double xScale = (double) roiWidth / width;
        int[] pixels2 = (int[]) getPixelsCopy();
        int index1, index2, xsi, ysi;
        double ys, xs;
        for (int y = ymin; y <= ymax; y++) {
            ys = (y - yCenter) / yScale + yCenter;
            ysi = (int) ys;
            index1 = y * width + xmin;
            index2 = width * (int) ys;
            for (int x = xmin; x <= xmax; x++) {
                xs = (x - xCenter) / xScale + xCenter;
                xsi = (int) xs;
                if (((xsi < xmin) || (xsi > xmax) || (ysi < ymin) || (ys > ymax))) {
                    System.out.println(x + "-" + xs + "-" + xsi
                            + ((xsi < xmin) || (xsi > xmax)));
                    pixels2[index1++] = (byte) bgColor;
                } else {
                    pixels2[index1++] = this.pixels[index2 + xsi];
                }
            }
        }
        this.pixels = pixels2;
        return this;
    }

    public void setRGB(byte[] R, byte[] G, byte[] B) {
        for (int i = 0; i < this.width * this.height; i++)
            this.pixels[i] = (0xFF000000 | (R[i] & 0xFF) << 16
                    | (G[i] & 0xFF) << 8 | B[i] & 0xFF);
    }

    public void applyTable(int[] lut) {
        for (int y = this.roiY; y < this.roiY + this.roiHeight; y++) {
            int i = y * this.width + this.roiX;
            for (int x = this.roiX; x < this.roiX + this.roiWidth; x++) {
                int c = this.pixels[i];
                int r = lut[((c & 0xFF0000) >> 16)];
                int g = lut[((c & 0xFF00) >> 8)];
                int b = lut[(c & 0xFF)];
                this.pixels[i] = (-16777216 + (r << 16) + (g << 8) + b);
                i++;
            }
        }
    }

    public void applyTable(int[] lut, int channels) {
        int r = 0;
        int g = 0;
        int b = 0;
        for (int y = this.roiY; y < this.roiY + this.roiHeight; y++) {
            int i = y * this.width + this.roiX;
            for (int x = this.roiX; x < this.roiX + this.roiWidth; x++) {
                int c = this.pixels[i];
                if (channels == 4) {
                    r = lut[((c & 0xFF0000) >> 16)];
                    g = (c & 0xFF00) >> 8;
                    b = c & 0xFF;
                } else if (channels == 2) {
                    r = (c & 0xFF0000) >> 16;
                    g = lut[((c & 0xFF00) >> 8)];
                    b = c & 0xFF;
                } else if (channels == 1) {
                    r = (c & 0xFF0000) >> 16;
                    g = (c & 0xFF00) >> 8;
                    b = lut[(c & 0xFF)];
                } else if ((channels & 0x6) == 6) {
                    r = lut[((c & 0xFF0000) >> 16)];
                    g = lut[((c & 0xFF00) >> 8)];
                    b = c & 0xFF;
                } else if ((channels & 0x5) == 5) {
                    r = lut[((c & 0xFF0000) >> 16)];
                    g = (c & 0xFF00) >> 8;
                    b = lut[(c & 0xFF)];
                } else if ((channels & 0x3) == 3) {
                    r = (c & 0xFF0000) >> 16;
                    g = lut[((c & 0xFF00) >> 8)];
                    b = lut[(c & 0xFF)];
                }
                this.pixels[i] = (-16777216 + (r << 16) + (g << 8) + b);
                i++;
            }
        }
    }

    public void substractImage(JPEGImageProcessor other) {
        int[] subImage = new int[pixels.length];
        for (int i = 0; i < pixels.length; i++) {
            Color color = new Color(pixels[i]);
            Color color1 = new Color(other.pixels[i]);
            int r = color.getRed() - color1.getRed();
            int g = color.getGreen() - color1.getGreen();
            int b = color.getBlue() - color1.getBlue();
            try {
                Color subColor = new Color(r < 0 ? 0 : r, g < 0 ? 0 : g,
                        b < 0 ? 0 : b);
                subImage[i] = subColor.getRGB();
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println(r + "," + g + "," + b);
            }
        }
        System.arraycopy(subImage, 0, pixels, 0, pixels.length);
    }

    public void setMinAndMax(double min, double max) {
        setMinAndMax(min, max, 7);
    }

    public void setMinAndMax(double min, double max, int channels) {
        LOGGER.debug("Setting min and max to " + min + "," + max);
        if (max < min)
            return;
        this.min = (int) min;
        this.max = (int) max;

        int[] lut = new int[256];
        for (int i = 0; i < 256; i++) {
            int v = i - this.min;
            v = (int) (256.0D * v / (max - min));
            if (v < 0)
                v = 0;
            if (v > 255)
                v = 255;
            lut[i] = v;
        }
        reset();
        if (channels == 7)
            applyTable(lut);
        else
            applyTable(lut, channels);
    }

    public int[] getHistogram() {
        int[] histogram = new int[256];
        for (int y = this.roiY; y < this.roiY + this.roiHeight; y++) {
            int i = y * this.width + this.roiX;
            for (int x = this.roiX; x < this.roiX + this.roiWidth; x++) {
                int c = this.pixels[(i++)];
                int r = (c & 0xFF0000) >> 16;
                int g = (c & 0xFF00) >> 8;
                int b = c & 0xFF;
                int v = (int) (r * rWeight + g * gWeight + b * bWeight + 0.5D);
                histogram[v] += 1;
            }
        }
        return histogram;
    }

    /**
     * Even though the name says auto adjust contrast, it basically fixes white
     * balance, as per algorithm in Gimp
     * (http://docs.gimp.org/2.6/en/gimp-layer-white-balance.html)
     */
    public void autoAdjustContrast() {
        int pixelCount = width * height;
        int limit = pixelCount / 10;
        int[] histogram = getHistogram();
        int autoThreshold = 0;
        if (autoThreshold < 10)
            autoThreshold = 2000;
        else
            autoThreshold /= 2;
        int threshold = pixelCount / autoThreshold; // 0.05% of all pixels
        int i = -1;
        boolean found = false;
        int count;
        do {
            i++;
            count = histogram[i];
            if (count > limit)
                count = 0;
            found = count > threshold;
        } while ((!found) && (i < 255));
        int hmin = i;
        i = 256;
        do {
            i--;
            count = histogram[i];
            if (count > limit)
                count = 0;
            found = count > threshold;
        } while ((!found) && (i > 0));
        int hmax = i;
        double histMin = 0.0D;
        double binSize = 1.0D;
        double cmin;
        double cmax;
        if (hmax >= hmin) {
            cmin = (histMin + hmin * binSize);
            cmax = (histMin + hmax * binSize);
            if (cmin == cmax) {
                cmin = 0;
                cmax = 255.0D;
            }
            setMinAndMax(cmin, cmax);
        }
    }

    public void changeHSB(float hMultiplier, float sMultiplier,
            float bMultiplier) {
        reset();
        for (int i = 0; i < pixels.length; i++) {
            int c = pixels[i];
            int value = 0xff000000 | c;
            int r = (value >> 16) & 0xFF;
            int g = (value >> 8) & 0xFF;
            int b = (value >> 0) & 0xFF;
            float[] hsb = Color.RGBtoHSB(r, g, b, null);
            float modHue = hMultiplier * hsb[0];
            float modSat = sMultiplier * hsb[1];
            float modBright = bMultiplier * hsb[2];
            float[] newVals = new float[] { modHue > 1 ? 1 : modHue,
                    modSat > 1 ? 1 : modSat, modBright > 1 ? 1 : modBright };
            int mc = Color.HSBtoRGB(newVals[0], newVals[1], newVals[2]);
            pixels[i] = mc;
        }
    }

    public void process(Operation op, double value) {
        double SCALE = 255.0D / Math.log(255.0D);
        int[] lut = new int[256];
        for (int i = 0; i < 256; i++) {
            int v;
            switch (op) {
            case GAMMA:
                v = (int) (Math.exp(Math.log(i / 255.0D) * value) * 255.0D);
                break;
            default:
                v = i;
            }
            if (v < 0)
                v = 0;
            if (v > 255)
                v = 255;
            lut[i] = v;
        }
        applyTable(lut);
    }

    public void snapshot() {
        if ((this.snapshotPixels == null)
                || ((this.snapshotPixels != null) && (this.snapshotPixels.length != this.pixels.length)))
            this.snapshotPixels = new int[this.width * this.height];
        System.arraycopy(this.pixels, 0, this.snapshotPixels, 0, this.width
                * this.height);
        this.snapshotMax = this.max;
        this.snapshotMin = this.min;
    }

    public void adjustBrightness(int bvalue) {
        this.brightness = bvalue;
        apply();
    }

    public void increaseExposureBy(int comp) {
        this.exposure = comp;
        apply();
    }

    public void increaseShadowsBy(int comp) {
        this.shadows = comp;
        apply();
    }

    public void apply() {
        resetRoi();
        reset();
        float c = (exposure + 50) / 50.0f;
        int[] lut = new int[256];
        for (int i = 0; i < 256; i++) {
            int v = (int) (i * c); // Exposure
            LOGGER.info("v after exposure= " + v + "," + exposure);
            v = v - shadows;
            v = (int) (256.0D * v / (255 - shadows));
            LOGGER.info("v after shadows= " + v + "," + shadows);
            if (brightness != 0)
                v = (int) (255 * (1 - (float) Math.exp(-(v / 255.0f)
                        * brightness)));
            LOGGER.info("v after brightness= " + v + "," + brightness);
            float cv = v/255.0f;
            v = (int) (255 * ((cv - 0.5f) * contrast + 0.5f));
            LOGGER.info("v after contrast= " + v+ "," + contrast);
            if (v < 0)
                v = 0;
            if (v > 255)
                v = 255;
            lut[i] = v;
        }
        for (int y = this.roiY; y < this.roiY + this.roiHeight; y++) {
            int i = y * this.width + this.roiX;
            for (int x = this.roiX; x < this.roiX + this.roiWidth; x++) {
                int c1 = this.pixels[i];
                int r = lut[((c1 & 0xFF0000) >> 16)];
                int g = lut[((c1 & 0xFF00) >> 8)];
                int b = lut[(c1 & 0xFF)];
                int v = (r + g + b) / 3; // or a better brightness calculation
                                         // if you prefer
                r = PixelUtils.clamp((int) (v + saturation * (r - v)));
                g = PixelUtils.clamp((int) (v + saturation * (g - v)));
                b = PixelUtils.clamp((int) (v + saturation * (b - v)));
                this.pixels[i] = (-16777216 + (r << 16) + (g << 8) + b);
                i++;
            }
        }
    }
}
